# Part of Odoo. See LICENSE file for full copyright and licensing details.

from unittest.mock import patch

from odoo.tests import JsonRpcException, tagged
from odoo.tools import mute_logger

from odoo.addons.account_payment.tests.common import AccountPaymentCommon
from odoo.addons.http_routing.tests.common import MockRequest
from odoo.addons.mail.tests.common import MailCase
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.addons.sale.tests.common import SaleCommon


@tagged('-at_install', 'post_install')
class TestSalePayment(AccountPaymentCommon, MailCase, PaymentHttpCommon, SaleCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # Replace PaymentCommon defaults by SaleCommon ones
        cls.currency = cls.sale_order.currency_id
        cls.partner = cls.sale_order.partner_invoice_id

        cls.provider.journal_id.inbound_payment_method_line_ids.filtered(
            lambda l: l.payment_provider_id == cls.provider
        ).payment_account_id = cls.inbound_payment_method_line.payment_account_id

        cls.sale_order.require_payment = True

    @mute_logger('odoo.http', 'werkzeug')
    def test_payment_amount_must_not_be_less_than_prepayment_amount(self):
        """ Test that accessing the portal page with a payment amount below prepayment amount raises
        an error. """
        res = self._make_http_get_request(f'/my/orders/{self.sale_order.id}', params={
            'access_token': self.sale_order._portal_ensure_token(), 'payment_amount': 1
        })
        self.assertEqual(res.status_code, 404)

    def test_is_down_payment_when_prepayment_amount_is_less_than_order_total(self):
        """Test that we are in the downpayment case when the prepayment amount is less than the
        order total."""
        self.sale_order.prepayment_percent = 0.5
        self.assertTrue(CustomerPortal()._determine_is_down_payment(
            self.sale_order, 'whatever', None
        ))

    def test_is_not_down_payment_when_prepayment_amount_equals_order_total(self):
        """Test that we are not in the downpayment case when the prepayment amount equals the order
        total."""
        self.sale_order.prepayment_percent = 1.0
        self.assertFalse(CustomerPortal()._determine_is_down_payment(
            self.sale_order, 'whatever', None
        ))

    def test_is_down_payment_when_link_amount_is_less_than_order_total(self):
        """Test that we are in the downpayment case when the link amount is less than the order
        total."""
        self.assertTrue(CustomerPortal()._determine_is_down_payment(
            self.sale_order, 'whatever', self.sale_order.amount_total * 0.5
        ))

    def test_is_not_down_payment_when_link_amount_equals_order_total(self):
        """Test that we are not in the downpayment case when the link amount equals the order total.
        """
        self.assertFalse(CustomerPortal()._determine_is_down_payment(
            self.sale_order, 'whatever', self.sale_order.amount_total
        ))

    def test_downpayment_amount_equals_link_amount_when_higher_than_prepayment_amount(self):
        """Test that the payment link's amount is used for the transaction when that amount is
        higher than the prepayment amount and the user chose to pay a down payment."""
        self.sale_order.prepayment_percent = 0.5  # This should be ignored when the link is higher.
        link_amount = self.sale_order.amount_total * 0.7
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order, is_down_payment=True, payment_amount=link_amount
            )
        self.assertEqual(tx_values['amount'], link_amount)

    def test_downpayment_amount_equals_prepayment_amount_when_less_than_order_total(self):
        """Test that the payment link's amount is used for the transaction when that amount is
        higher than the prepayment amount and the user chose to pay a down payment."""
        self.sale_order.prepayment_percent = 0.5
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order, is_down_payment=True, payment_amount=self.sale_order.amount_total
            )
        self.assertEqual(tx_values['amount'], self.sale_order._get_prepayment_required_amount())

    def test_downpayment_amount_equals_prepayment_amount_when_no_link_amount(self):
        """Test that the prepayment amount is used for the transaction when no payment amount is
        specified in the link and the user chose to pay a down payment."""
        self.sale_order.prepayment_percent = 0.5
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order, is_down_payment=True, payment_amount=None
            )
        prepayment_amount = self.sale_order._get_prepayment_required_amount()
        self.assertEqual(tx_values['amount'], prepayment_amount)

    def test_payment_amount_equals_link_amount_when_order_is_confirmed(self):
        """Test that the payment link's amount is used for the transaction when the order is
        confirmed."""
        self.sale_order.action_confirm()
        payment_amount = self.sale_order.amount_total * 0.5
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order, is_down_payment=False, payment_amount=payment_amount
            )
        self.assertEqual(tx_values['amount'], payment_amount)

    def test_payment_amount_equals_order_total_when_no_link_amount_and_order_is_confirmed(self):
        """Test that the order total is used for the transaction when no payment amount is specified
        in the link and the order is confirmed."""
        self.sale_order.action_confirm()
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order, is_down_payment=False, payment_amount=None
            )
        self.assertEqual(tx_values['amount'], self.sale_order.amount_total)

    def test_full_amount_equals_order_total(self):
        """Test that the order total is used for the transaction when the user chose to pay the full
        amount. """
        self.sale_order.prepayment_percent = 0.5  # This should not impact the 'full amount' option.
        with MockRequest(self.env):
            tx_values = CustomerPortal()._get_payment_values(
                self.sale_order,
                is_down_payment=False,
                payment_amount=self.sale_order._get_prepayment_required_amount()
            )
        self.assertEqual(tx_values['amount'], self.sale_order.amount_total)

    def test_confirmed_transactions_comfirms_so_with_multiple_transaction(self):
        """ Test that a confirmed transaction confirms a SO even if one or more non-confirmed
        transactions are linked. """
        # Create the payment
        self.amount = self.sale_order.amount_total
        self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='draft',
            reference='Test Transaction Draft 1',
        )
        self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='draft',
            reference='Test Transaction Draft 2',
        )
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')

    def test_auto_confirm_and_auto_invoice(self):
        """
        Assuming that the automatic invoice setting is activated, we expect
        that after the payment is post processed:
        - invoice created
        - SO confirmed
        - Two emails sent: SO confirmation and default invoice email template
        """
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total
        self.partner.email = 'customer@example.com'  # make sure partner on SO has email set
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with (
            mute_logger('odoo.addons.sale.models.payment_transaction'),
            self.mock_mail_gateway(),
        ):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)
        self.assertEqual(len(self._new_mails), 2)
        self.assertTrue(self._new_mails.filtered(lambda x: 'Invoice' in x.subject))

    def test_auto_confirm_and_auto_invoice_custom_mail_template(self):
        """
        Assuming that the automatic invoice setting is activated and a custom
        email template for invoicing was selected, we expect that after the
        payment is post processed:
        - invoice created
        - SO confirmed
        - Two emails sent: SO confirmation and invoice email using the custom template
        """
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
        custom_template = self.env['mail.template'].create({
            'name': 'Custom Test Invoice Template',
            'model_id': self.env.ref('account.model_account_move').id,
            'subject': 'Your Custom Template',
            'partner_to': '{{ object.partner_id.id }}',
            'email_from': '{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}',
        })
        self.env['ir.config_parameter'].set_param('sale.default_invoice_email_template', custom_template.id)

        # Create the payment
        self.amount = self.sale_order.amount_total
        self.partner.email = 'customer@example.com'  # make sure partner on SO has email set
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with (
            mute_logger('odoo.addons.sale.models.payment_transaction'),
            self.mock_mail_gateway(),
        ):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)
        self.assertEqual(len(self._new_mails), 2)
        self.assertTrue(self._new_mails.filtered(lambda x: 'Your Custom Template' in x.subject))

    def test_auto_confirm_and_auto_invoice_custom_mail_template_unlinked(self):
        """
        Assuming that the automatic invoice setting is activated and a custom
        email template for invoicing was selected. If the custom email template
        gets unlinked, the system parameter still stores the id, but code
        should fall back to default invoice email template. We expect that after the
        payment is post processed:
        - invoice created
        - SO confirmed
        - Two emails sent: SO confirmation and invoice email using the DEFAULT template
        """
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
        custom_template = self.env['mail.template'].create({
            'name': 'Custom Test Invoice Template',
            'model_id': self.env.ref('account.model_account_move').id,
            'subject': 'Your Custom Template',
            'partner_to': '{{ object.partner_id.id }}',
            'email_from': '{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}',
        })
        self.env['ir.config_parameter'].set_param('sale.default_invoice_email_template', custom_template.id)
        custom_template.unlink()

        # Create the payment
        self.amount = self.sale_order.amount_total
        self.partner.email = 'customer@example.com'  # make sure partner on SO has email set
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with (
            mute_logger('odoo.addons.sale.models.payment_transaction'),
            self.mock_mail_gateway(),
        ):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)
        self.assertEqual(len(self._new_mails), 2)
        self.assertTrue(self._new_mails.filtered(lambda x: 'Invoice' in x.subject))

    def test_auto_done_and_auto_invoice(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
        # Lock the sale orders when confirmed
        self.group_user.implied_ids += self.env.ref('sale.group_auto_done_setting')

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(self.sale_order.locked)
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)
        self.assertTrue(tx.invoice_ids.is_move_sent)

    def test_so_partial_payment_no_invoice(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total / 10.
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'draft')
        self.assertFalse(tx.invoice_ids)
        self.assertFalse(self.sale_order.invoice_ids)

    def test_already_confirmed_so_payment(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Confirm order before payment
        self.sale_order.action_confirm()

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        tx._post_process()

        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)

    def test_invoice_is_final(self):
        """Test that invoice generated from a payment are always final"""
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'), patch(
            'odoo.addons.sale.models.sale_order.SaleOrder._create_invoices',
            return_value=self.env['account.move']
        ) as _create_invoices_mock:
            tx._post_process()

        self.assertTrue(_create_invoices_mock.call_args.kwargs['final'])

    def test_linked_transactions_when_invoicing(self):
        self.provider.support_manual_capture = 'partial'
        partial_amount = self.sale_order.amount_total - 2

        partial_tx_done = self._create_transaction(
            flow='direct',
            amount=partial_amount,
            sale_order_ids=[self.sale_order.id],
            state='done',
            reference='partial_tx_done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            partial_tx_done._post_process()
        partial_tx_pending = self._create_transaction(
            flow='direct',
            amount=2,
            sale_order_ids=[self.sale_order.id],
            state='pending',
            reference='partial_tx_pending',
        )
        self.assertTrue(partial_tx_done.payment_id, msg="Account payment should have been created.")
        msg = "The created account payment shouldn't be reconciled as there are no invoice yet."
        self.assertFalse(partial_tx_pending.payment_id.is_reconciled, msg=msg)

        # Add some noisy transactions
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='draft', reference='draft_tx'
        )
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='error', reference='error_tx'
        )
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='cancel', reference='cncl_tx'
        )

        msg = "The sale order should be linked to 5 transactions."
        self.assertEqual(len(self.sale_order.transaction_ids), 5, msg=msg)

        self.sale_order.action_confirm()
        self.sale_order._create_invoices()

        self.assertEqual(len(self.sale_order.invoice_ids), 1, msg="1 invoice should be created.")

        first_invoice = self.sale_order.invoice_ids
        linked_txs = first_invoice.transaction_ids
        msg = "The newly created invoice should be linked to the done and pending transactions."
        self.assertEqual(len(linked_txs), 2, msg=msg)
        expected_linked_tx = (partial_tx_done, partial_tx_pending)
        self.assertTrue(all(tx in expected_linked_tx for tx in linked_txs), msg=msg)
        msg = "The payment shouldn't be reconciled yet."
        self.assertFalse(partial_tx_done.payment_id.is_reconciled, msg=msg)

        partial_tx_done._post_process()

        msg = "The payment should now be reconciled."
        self.assertTrue(partial_tx_done.payment_id.is_reconciled, msg=msg)

        self.sale_order.order_line[0].product_uom_qty += 2
        self.sale_order._create_invoices()

        second_invoice = self.sale_order.invoice_ids - first_invoice
        msg = "The newly created invoice should only be linked to the pending transaction."
        self.assertEqual(len(second_invoice.transaction_ids), 1, msg=msg)
        self.assertEqual(second_invoice.transaction_ids.state, 'pending', msg=msg)

    def test_downpayment_confirm_sale_order_sufficient_amount(self):
        """Paying down payments can confirm an order if amount is enough."""
        self.sale_order.prepayment_percent = 0.1
        order_amount = self.sale_order.amount_total

        tx = self._create_transaction(
            flow='direct',
            amount=order_amount * self.sale_order.prepayment_percent,
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertTrue(self.sale_order.state == 'sale')

    def test_downpayment_automatic_invoice(self):
        """
        Down payment invoices should be created when a down payment confirms
        the order and automatic invoice is checked.
        """
        self.sale_order.prepayment_percent = 0.2
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        tx = self._create_transaction(
            flow='direct',
            amount=self.sale_order.amount_total * self.sale_order.prepayment_percent,
            sale_order_ids=[self.sale_order.id],
            state='done')

        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        invoice = self.sale_order.invoice_ids
        self.assertTrue(len(invoice) == 1)
        self.assertTrue(invoice.line_ids[0].is_downpayment)

    @mute_logger('odoo.http')
    def test_transaction_route_rejects_unexpected_kwarg(self):
        url = self._build_url(f'/my/orders/{self.sale_order.id}/transaction')
        route_kwargs = {
            'access_token': self.sale_order._portal_ensure_token(),
            'partner_id': self.partner.id,  # This should be rejected.
        }
        with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
            self.make_jsonrpc_request(url, route_kwargs)

    def test_partial_payment_confirm_order(self):
        """
        Test that a sale order can be confirmed through partial payments and that
        correct mails are sent each time.
        """
        self.amount = self.sale_order.amount_total / 2

        with patch(
            'odoo.addons.sale.models.sale_order.SaleOrder._send_order_notification_mail',
        ) as notification_mail_mock:
            tx_pending = self._create_transaction(
                flow='direct',
                sale_order_ids=[self.sale_order.id],
                state='pending',
                reference='Test Transaction Draft 1',
            )

            self.assertEqual(self.sale_order.state, 'draft')

            tx_pending._set_done()
            tx_pending._post_process()

            self.assertEqual(notification_mail_mock.call_count, 1)
            notification_mail_mock.assert_called_once_with(
                self.env.ref('sale.mail_template_sale_payment_executed'))
            self.assertEqual(self.sale_order.state, 'draft')
            self.assertEqual(self.sale_order.amount_paid, self.amount)

            tx_done = self._create_transaction(
                flow='direct',
                sale_order_ids=[self.sale_order.id],
                state='done',
                reference='Test Transaction Draft 2',
            )
            tx_done._post_process()

            self.assertEqual(notification_mail_mock.call_count, 2)
            order_confirmation_mail_template_id = int(
                self.env["ir.config_parameter"]
                .sudo()
                .get_param("sale.default_confirmation_template", self.env.ref("sale.mail_template_sale_confirmation").id)
            )
            notification_mail_mock.assert_called_with(self.env["mail.template"].browse(order_confirmation_mail_template_id))
            self.assertEqual(self.sale_order.state, 'sale')

    def test_automatic_invoice_mail_author(self):
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        portal_user = self.env['res.users'].create({
            'name': 'Portal Customer',
            'login': 'portal@test.com',
            'email': 'portal@test.com',
            'partner_id': self.partner_a.id,
            'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])],
        })
        portal_user.partner_id.invoice_sending_method = 'email'

        sale_order = self.env['sale.order'].with_user(portal_user).sudo().create({
            'partner_id': portal_user.partner_id.id,
            'user_id': self.sale_user.id,
            'order_line': [(0, 0, {
                'product_id': self.product_a.id,
                'product_uom_qty': 1,
                'price_unit': 100.0,
            })],
        })

        self.amount = sale_order.amount_total
        tx = self._create_transaction(
            flow='redirect',
            sale_order_ids=[sale_order.id],
            state='done'
        )

        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx.with_user(portal_user).sudo()._post_process()

        # Verify invoice was created and sent successfully
        invoice = sale_order.invoice_ids[0]
        self.assertTrue(invoice.is_move_sent, "Invoice should be marked as sent")
        invoice_sent_mail = invoice.message_ids[0]
        self.assertTrue(invoice_sent_mail.author_id.id not in invoice_sent_mail.notified_partner_ids.ids)
